package jamirr;

import java.util.*;

public abstract class AbstractBeanMirror<B> extends AnnotatedElementImpl implements BeanMirror<B> {

    private final Class<B> beanType;
    private final List<ConstructorMirror<B>> constructors;
    private final PropertyMirror<B, Object>[] properties;
    private final List<PropertyMirror<B, Object>> propertiesList;
    private final List<MethodMirror<B, Object>> methods;
    private final StringIntMap propertyIndexMap;

    @SuppressWarnings("unchecked")
    protected AbstractBeanMirror(Class<B> beanType,
                                 AnnotationMirror[] declaredAnnotations,
                                 ConstructorRef[] constructorRefs,
                                 PropertyRef<Object>[] propertyRefs,
                                 MethodRef<Object>[] methodRefs) {
        super(declaredAnnotations);
        this.beanType = beanType;
        if (constructorRefs != null) {
            List<ConstructorMirror<B>> constructorMirrors = new ArrayList<>(constructorRefs.length);
            for (ConstructorRef constructorRef : constructorRefs) {
                constructorMirrors.add(new ConstructorMirrorImpl(constructorRef));
            }
            this.constructors = constructorMirrors;
        } else {
            this.constructors = Collections.emptyList();
        }

        if (propertyRefs != null) {
            List<PropertyMirror<B, Object>> propertyMirrors = new ArrayList<>(propertyRefs.length);
            for (PropertyRef<Object> propertyRef : propertyRefs) {
                propertyMirrors.add(new PropertyMirrorImpl<>(propertyRef));
            }
            this.properties = propertyMirrors.toArray(PropertyMirror[]::new);
            this.propertiesList = propertyMirrors;
        } else {
            this.properties = new PropertyMirror[0];
            this.propertiesList = Collections.emptyList();
        }
        this.propertyIndexMap = new StringIntMap(this.properties.length);
        for (int i = 0; i < this.properties.length; i++) {
            this.propertyIndexMap.put(this.properties[i].getName(), i);
        }

        if (methodRefs != null) {
            List<MethodMirror<B, Object>> methodMirrors = new ArrayList<>(methodRefs.length);
            for (MethodRef<Object> methodRef : methodRefs) {
                methodMirrors.add(new MethodMirrorImpl<>(methodRef));
            }
            this.methods = methodMirrors;
        } else {
            this.methods = Collections.emptyList();
        }
    }

    @Override
    public Class<B> getBeanType() {
        return beanType;
    }

    @Override
    public Collection<ConstructorMirror<B>> getConstructors() {
        return constructors == null ? List.of() : constructors;
    }

    @Override
    public Collection<PropertyMirror<B, Object>> getProperties() {
        return propertiesList == null ? List.of() : propertiesList;
    }

    @Override
    public Collection<MethodMirror<B, Object>> getMethods() {
        return methods == null ? List.of() : methods;
    }

    @Override
    public int propertyIndexOf(String name) {
        return propertyIndexMap.get(name, -1);
    }

    /**
     * Obtain a property by its index.
     *
     * @param index The index of the property
     * @return A bean property
     */
    protected PropertyMirror<B, Object> getPropertyByIndex(int index) {
        return this.properties[index];
    }

    /**
     * Triggers the invocation of the constructor at index.
     *
     * @param index The constructor index
     * @param args  The arguments
     * @return The result
     */
    protected B dispatchInstance(int index, Object[] args) {
        throw unknownDispatchAtIndexException(index);
    }

    /**
     * Triggers the invocation of the method at index.
     *
     * @param index  The method index
     * @param target The target
     * @param args   The arguments
     * @param <V>    The result type
     * @return The result
     */
    protected <V> V dispatchMethod(int index, Object target, Object[] args) {
        throw unknownDispatchAtIndexException(index);
    }

    /**
     * Triggers the invocation of the method at index for a single argument call.
     * Allowing to not wrap a single argument in an object array.
     *
     * @param index  The method index
     * @param target The target
     * @param arg    The argument
     * @param <V>    The result type
     * @return The result
     */
    protected <V> V dispatchOne(int index, Object target, Object arg) {
        return dispatchMethod(index, target, new Object[]{arg});
    }

    /**
     * Creates a new exception when the dispatch at index is not found.
     *
     * @param index The method index
     * @return The exception
     */
    @SuppressWarnings("all")
    protected RuntimeException unknownDispatchAtIndexException(int index) {
        return new IllegalStateException("Unknown dispatch at index: " + index);
    }

    protected static final class ConstructorRef {
        final int constructorIndex;
        final Argument<?>[] arguments;
        final AnnotationMirror[] declaredAnnotations;

        public ConstructorRef(int constructorIndex,
                              Argument<?>[] arguments,
                              AnnotationMirror[] declaredAnnotations) {
            this.constructorIndex = constructorIndex;
            this.arguments = arguments;
            this.declaredAnnotations = declaredAnnotations;
        }
    }

    protected static final class PropertyRef<P> {
        final Argument<P> argument;
        final AnnotationMirror[] declaredAnnotations;
        final boolean readable;
        final int getMethodIndex;
        final boolean writable;
        final int setMethodIndex;

        public PropertyRef(Argument<P> argument,
                           boolean readable,
                           int getMethodIndex,
                           boolean writable,
                           int setMethodIndex,
                           AnnotationMirror[] declaredAnnotations) {
            this.argument = argument;
            this.readable = readable;
            this.getMethodIndex = getMethodIndex;
            this.writable = writable;
            this.setMethodIndex = setMethodIndex;
            this.declaredAnnotations = declaredAnnotations;
        }
    }

    protected static final class MethodRef<R> {
        final int methodIndex;
        final Argument<R> returnType;
        final String name;
        final AnnotationMirror[] declaredAnnotations;
        final Argument<?>[] arguments;

        public MethodRef(int methodIndex,
                         Argument<R> returnType,
                         String name,
                         Argument<?>[] arguments,
                         AnnotationMirror[] declaredAnnotations) {
            this.methodIndex = methodIndex;
            this.returnType = returnType;
            this.name = name;
            this.arguments = arguments;
            this.declaredAnnotations = declaredAnnotations;
        }
    }

    private class ConstructorMirrorImpl extends AnnotatedElementImpl implements ConstructorMirror<B> {

        private final ConstructorRef ref;

        public ConstructorMirrorImpl(ConstructorRef ref) {
            super(ref.declaredAnnotations);
            this.ref = ref;
        }

        @Override
        public BeanMirror<B> getDeclaringBean() {
            return AbstractBeanMirror.this;
        }

        @Override
        public Argument<?>[] getArguments() {
            return ref.arguments == null ? Argument.ZERO_ARGUMENTS : ref.arguments;
        }

        @Override
        public B invoke(Object... parameterValues) {
            return dispatchInstance(ref.constructorIndex, parameterValues);
        }
    }

    private class PropertyMirrorImpl<P> extends AnnotatedElementImpl implements PropertyMirror<B, P> {

        private final PropertyRef<P> ref;
        private final Class<?> typeOrWrapperType;

        private PropertyMirrorImpl(PropertyRef<P> ref) {
            super(ref.declaredAnnotations);
            this.ref = ref;
            this.typeOrWrapperType = Utils.getWrapperType(getPropertyType());
        }

        @Override
        public BeanMirror<B> getDeclaringBean() {
            return AbstractBeanMirror.this;
        }

        @Override
        public String getName() {
            return ref.argument.getName();
        }

        @Override
        public Class<P> getPropertyType() {
            return ref.argument.getType();
        }

        @Override
        public P get(B instance) {
            Objects.requireNonNull(instance, "instance cannot be null");
            if (!AbstractBeanMirror.this.beanType.isInstance(instance)) {
                throw new IllegalArgumentException("Invalid bean [" + instance + "] for type: " + AbstractBeanMirror.this.beanType);
            }
            if (isWriteOnly()) {
                throw new UnsupportedOperationException("Cannot read from a write-only property: " + getName());
            }
            return dispatchOne(ref.getMethodIndex, instance, null);
        }

        @Override
        public void set(B instance, P value) {
            Objects.requireNonNull(instance, "instance cannot be null");
            if (!AbstractBeanMirror.this.beanType.isInstance(instance)) {
                throw new IllegalArgumentException("Invalid bean [" + instance + "] for type: " + AbstractBeanMirror.this.beanType);
            }
            if (isReadOnly()) {
                throw new UnsupportedOperationException("Cannot write a read-only property: " + getName());
            }
            if (value != null && !typeOrWrapperType.isInstance(value)) {
                throw new IllegalArgumentException("Specified value [" + value + "] is not of the correct type: " + getPropertyType());
            }
            dispatchOne(ref.setMethodIndex, instance, value);
        }
    }

    private final class MethodMirrorImpl<P> extends AnnotatedElementImpl implements MethodMirror<B, P> {

        private final MethodRef<P> ref;

        private MethodMirrorImpl(MethodRef<P> ref) {
            super(ref.declaredAnnotations);
            this.ref = ref;
        }

        @Override
        public BeanMirror<B> getDeclaringBean() {
            return AbstractBeanMirror.this;
        }

        @Override
        public String getName() {
            return ref.name;
        }

        @Override
        public ReturnType<P> getReturnType() {
            return new ReturnType<>() {

                @Override
                public Class<P> getType() {
                    return ref.returnType.getType();
                }

                @Override
                public Argument<P> asArgument() {
                    return ref.returnType;
                }

                @Override
                public Map<String, Argument<?>> getTypeVariables() {
                    return ref.returnType.getTypeVariables();
                }
            };
        }

        @Override
        public Argument<?>[] getArguments() {
            return ref.arguments == null ? Argument.ZERO_ARGUMENTS : ref.arguments;
        }

        @Override
        public P invoke(B instance, Object... arguments) {
            return dispatchMethod(ref.methodIndex, instance, arguments);
        }
    }
}
